/*
 * @:
 * @Version: 1.0
 * @Autor: ziwei
 * @Date: 2021-06-21 15:01:07
 * @LastEditors: ziwei
 * @LastEditTime: 2022-01-25 14:33:21
 */
const path = require('path');
const types = require('babel-types');
const parser = require('@babel/parser');
let traverse = require('@babel/traverse').default;
let generator = require('@babel/generator').default;
let fs = require('fs');
const baseDir = toUnixPath(process.cwd());
function toUnixPath(filePath) {
  return filePath.replace(/\\/g, '/');
}
class Complication {
  constructor(options) {
    this.options = options;
    this.modules = []; //存放本次编译产生的所有模块 所有的入口产出的模块
    this.chunks = []; //代码块的数组
    this.assets = {}; //产出的资源
    this.files = []; //所有产出的文件列表
    this.entrypoints = new Map(); //入口的数组
  }
  //这个才是编译最核心的方法
  build(callback) {
    //5. 根据配置的entry找到入口文件
    let entry = {};
    if (typeof this.options.entry === 'string') {
      entry.main = this.options.entry;
    } else {
      entry = this.options.entry;
    }
    //找到根目录
    let context = toUnixPath(__dirname);
    //entry={entry1:'./src/enter1.js,entry:'./src/entry2.js''}
    for (let entryName in entry) {
      //5.获取 entry1的绝对路径
      let entryFilePath = path.posix.join(context, entry[entryName]);
      //6. 从入口文件出发,调用所有配置的Loader对模块进行编译
      let entryModule = this.buildModule(entryName, entryFilePath);
      //8. 根据入口和模块之间的依赖关系，组装成一个个包含多个模块的 Chunk:模块的集合
      let chunk = {
        name: entryName, //代码块的名字就是入口的名字
        entryModule, //入口模块 entry.js
        modules: this.modules.filter((item) => item.name === entryName),
      };
      this.chunks.push(chunk);
      this.entrypoints.set(entryName, { name: entryName, chunks: [entryName] });
    }
    //9. 再把每个Chunk转换成一个单独的文件导加入到输出列表
    this.chunks.forEach((chunk) => {
      let filename = this.options.output.filename.replace('[name]', chunk.name);
      this.assets[filename] = getSource(chunk);
    });
    this.files = Object.keys(this.assets);
    //10 在确定好输出内容后，根据配置确定输出的路径和文件名，把文件内容写入到文件系统
    callback(null, {
      entrypoints: this.entrypoints,
      chunks: this.chunks,
      modules: this.modules,
      files: this.files,
      assets: this.assets,
    });
  }
  //name=名称(代码块的名字)：模块是属于哪个入口的 模块的绝对路径
  buildModule(name, modulePath) {
    //6. 从入口文件出发,调用所有配置的Loader对模块进行编译
    //1.读取模块文件的内容
    let sourceCode = fs.readFileSync(modulePath, 'utf8');
    let { rules } = this.options.module;
    let loaders = []; //寻找匹配的loader
    rules.forEach((rule) => {
      let { test } = rule;
      if (modulePath.match(test)) {
        loaders.push(...rule.use);
      }
    }); //loaders = [loggrt1,logger2]
    sourceCode = loaders.reduceRight((sourceCode, loader) => {
      return require(loader)(sourceCode);
    }, sourceCode);
    console.log('++sourceCode', sourceCode);
    //当前模块的模块ID
    let moduleId = './' + path.posix.relative(baseDir, modulePath);
    let module = { id: moduleId, dependencies: [], name };
    //7. 再找出该模块依赖的模块，再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
    let ast = parser.parse(sourceCode, { sourceType: 'module' });
    traverse(ast, {
      CallExpression: ({ node }) => {
        if ((node.callee.name === 'require')) {
          //依赖的模块的相对路径 webpack打包后不管什么模块，模块ID都是相对于相对路径./src ./mpde
          let depModuleName = node.arguments[0].value; //./title1
          //获取当前模块的所有的目录
          let dirname = path.posix.dirname(modulePath); //src
          //C:\aproject\XXXX\title.js
          let depModulePath = path.posix.join(dirname, depModuleName);
          let extensions = this.options.resolve.extensions;
          depModulePath = tryExtensions(depModulePath, extensions); //已经包含了拓展名了
          //相对于项目根目录的相对路径 生成此模块的ID
          let depModuleId = './' + path.posix.relative(baseDir, depModulePath);
          //把此模块依赖的模块ID和模块路径放到此模块依赖的数组中
          node.arguments = [types.stringLiteral(depModuleId)]; //title=>./src/title.js
          //把此模块依赖的模块ID和模块路径放到此模块的依赖数组中
          module.dependencies.push({ depModuleId, depModulePath });
        }
      },
    });
    let { code } = generator(ast); //根据改造后的语法树生成源代码
    module._source = code; //模块的源代码指向语法树转换后新生成的源代码

    //7.再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
    module.dependencies.forEach(({ depModuleId, depModulePath }) => {
      let depModule = this.buildModule(name, depModulePath);
      this.modules.push(depModule);
    });
    return module;
  }
}

function tryExtensions(modulePath, extensions) {
  //extensions.unshift("");
  if (fs.existsSync(modulePath)) {
    return modulePath;
  }
  for (let i = 0; i < extensions.length; i++) {
    let filePath = modulePath + extensions[i];
    if (fs.existsSync(filePath)) {
      return filePath;
    }
  }
  throw new Error(`${modulePath}没找到`);
}

function getSource(chunk) {
  return `
   (() => {
    var modules = {
      ${chunk.modules.map(
        (module) => `
        "${module.id}": (module) => {
          ${module._source}
        },
      `
      )}  
    };
    var cache = {};
    function require(moduleId) {
      var cachedModule = cache[moduleId];
      if (cachedModule !== undefined) {
        return cachedModule.exports;
      }
      var module = (cache[moduleId] = {
        exports: {},
      });
      modules[moduleId](module, module.exports, require);
      return module.exports;
    }
    var exports ={};
    ${chunk.entryModule._source}
  })();
  
   `;
}
module.exports = Complication;